package org.feisoft.jta;

import org.feisoft.common.utils.ByteUtils;
import org.feisoft.common.utils.CommonUtils;
import org.feisoft.jta.supports.wire.RemoteCoordinator;
import org.feisoft.transaction.*;
import org.feisoft.transaction.Transaction;
import org.feisoft.transaction.TransactionManager;
import org.feisoft.transaction.aware.TransactionBeanFactoryAware;
import org.feisoft.transaction.aware.TransactionEndpointAware;
import org.feisoft.transaction.xa.TransactionId;
import org.feisoft.transaction.xa.XidFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.transaction.*;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TransactionCoordinator
        implements RemoteCoordinator, TransactionBeanFactoryAware, TransactionEndpointAware
{

    static final Logger logger = LoggerFactory.getLogger(TransactionCoordinator.class);
    private final Lock lock = new ReentrantLock();
    @javax.inject.Inject
    private TransactionBeanFactory beanFactory;
    private String endpoint;
    @javax.inject.Inject
    private transient TransactionManager transactionManager;
    private transient boolean ready = false;

    public Transaction getTransactionQuietly()
    {
        TransactionManager transactionManager = this.beanFactory.getTransactionManager();
        return transactionManager.getTransactionQuietly();
    }

    public Transaction start(TransactionContext transactionContext, int flags) throws XAException
    {

        TransactionRepository transactionRepository = this.beanFactory.getTransactionRepository();
        TransactionManager transactionManager = this.beanFactory.getTransactionManager();
        if (transactionManager.getTransactionQuietly() != null)
        {
            throw new XAException(XAException.XAER_PROTO);
        }

        TransactionId globalXid = (TransactionId) transactionContext.getXid();
        Transaction transaction = transactionRepository.getTransaction(globalXid);
        if (transaction == null)
        {
            transaction = new TransactionImpl(transactionContext);
            ((TransactionImpl) transaction).setBeanFactory(this.beanFactory);

            long expired = transactionContext.getExpiredTime();
            long current = System.currentTimeMillis();
            long timeoutMillis = (expired - current) / 1000L;
            transaction.setTransactionTimeout((int) timeoutMillis);

            transactionRepository.putTransaction(globalXid, transaction);
        }

        transactionManager.associateThread(transaction);
        // this.transactionStatistic.fireBeginTransaction(transaction);

        return transaction;
    }

    public Transaction end(TransactionContext transactionContext, int flags) throws XAException
    {
        TransactionManager transactionManager = this.beanFactory.getTransactionManager();
        return transactionManager.desociateThread();
    }

    /**
     * supports resume only, for tcc transaction manager.
     */
    public void start(Xid xid, int flags) throws XAException
    {

        TransactionManager transactionManager = this.beanFactory.getTransactionManager();
        XidFactory xidFactory = this.beanFactory.getXidFactory();
        Transaction current = transactionManager.getTransactionQuietly();
        if (current != null)
        {
            throw new XAException(XAException.XAER_PROTO);
        }

        TransactionRepository transactionRepository = this.beanFactory.getTransactionRepository();

        TransactionId branchXid = (TransactionId) xid;
        TransactionId globalXid = xidFactory.createGlobalTransactionId(branchXid.getGlobalTransactionId());

        Transaction transaction = transactionRepository.getTransaction(globalXid);
        if (transaction == null)
        {
            throw new XAException(XAException.XAER_NOTA);
        }
        transactionManager.associateThread(transaction);
    }

    /**
     * supports suspend only, for tcc transaction manager.
     */
    public void end(Xid xid, int flags) throws XAException
    {
        if (TMSUSPEND != flags)
        {
            throw new XAException(XAException.XAER_INVAL);
        }
        TransactionManager transactionManager = this.beanFactory.getTransactionManager();
        XidFactory xidFactory = this.beanFactory.getXidFactory();
        Transaction transaction = transactionManager.getTransactionQuietly();
        if (transaction == null)
        {
            throw new XAException(XAException.XAER_NOTA);
        }
        TransactionContext transactionContext = transaction.getTransactionContext();
        TransactionId transactionId = transactionContext.getXid();

        TransactionId branchXid = (TransactionId) xid;
        TransactionId globalXid = xidFactory.createGlobalTransactionId(branchXid.getGlobalTransactionId());

        if (CommonUtils.equals(globalXid, transactionId) == false)
        {
            throw new XAException(XAException.XAER_INVAL);
        }
        transactionManager.desociateThread();
    }

    public void commit(Xid xid, boolean onePhase) throws XAException
    {
        this.checkParticipantReadyIfNecessary();

        XidFactory xidFactory = this.beanFactory.getXidFactory();
        TransactionId branchXid = (TransactionId) xid;
        TransactionId globalXid = xidFactory.createGlobalTransactionId(branchXid.getGlobalTransactionId());
        TransactionRepository repository = beanFactory.getTransactionRepository();
        Transaction transaction = repository.getTransaction(globalXid);
        if (transaction == null)
        {
            throw new XAException(XAException.XAER_NOTA);
        }

        try
        {
            transaction.participantPrepare();
            transaction.participantCommit(onePhase);
            transaction.forgetQuietly(); // forget transaction
        } catch (SecurityException ex)
        {
            logger.error("[{}] Error occurred while committing remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(ex);
            throw xaex;
        } catch (CommitRequiredException ex)
        {
            logger.error("[{}] Error occurred while committing remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(ex);
            throw xaex;
        } catch (RollbackException ex)
        {
            logger.error("[{}] Error occurred while committing remote coordinator, tx has been rolled back.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);

            // don't forget if branch-transaction has been hueristic completed.
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XA_HEURRB);
            xaex.initCause(ex);
            throw xaex;
        } catch (HeuristicMixedException ex)
        {
            logger.error("[{}] Error occurred while committing remote coordinator, tx has been completed mixed.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);

            // don't forget if branch-transaction has been hueristic completed.
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XA_HEURMIX);
            xaex.initCause(ex);
            throw xaex;
        } catch (HeuristicRollbackException ex)
        {
            logger.error(
                    "[{}] Error occurred while committing remote coordinator, tx has been rolled back heuristically.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);

            // don't forget if branch-transaction has been hueristic completed.
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XA_HEURRB);
            xaex.initCause(ex);
            throw xaex;
        } catch (SystemException ex)
        {
            logger.error("[{}] Error occurred while committing remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(ex);
            throw xaex;
        } catch (RuntimeException ex)
        {
            logger.error("[{}] Error occurred while committing remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(ex);
            throw xaex;
        }
    }

    public void forgetQuietly(Xid xid)
    {
        try
        {
            this.forget(xid);
        } catch (XAException ex)
        {
            switch (ex.errorCode)
            {
                case XAException.XAER_NOTA:
                    break;
                default:
                    logger.error("[{}] Error occurred while forgeting remote coordinator.",
                            ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            }
        } catch (RuntimeException ex)
        {
            logger.error("[{}] Error occurred while forgeting remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
        }
    }

    public void forget(Xid xid) throws XAException
    {
        this.checkParticipantReadyIfNecessary();

        if (xid == null)
        {
            throw new XAException(XAException.XAER_INVAL);
        }

        XidFactory xidFactory = this.beanFactory.getXidFactory();
        TransactionId branchXid = (TransactionId) xid;
        TransactionId globalXid = xidFactory.createGlobalTransactionId(branchXid.getGlobalTransactionId());
        TransactionRepository transactionRepository = beanFactory.getTransactionRepository();
        Transaction transaction = transactionRepository.getErrorTransaction(globalXid);
        if (transaction == null)
        {
            throw new XAException(XAException.XAER_NOTA);
        }

        try
        {
            transaction.forget();
        } catch (SystemException ex)
        {
            logger.error("[{}] Error occurred while forgeting remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            throw new XAException(XAException.XAER_RMERR);
        } catch (RuntimeException rex)
        {
            logger.error("[{}] Error occurred while forgeting remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), rex);
            throw new XAException(XAException.XAER_RMERR);
        }
    }

    public int getTransactionTimeout() throws XAException
    {
        return 0;
    }

    public boolean isSameRM(XAResource xares) throws XAException
    {
        throw new XAException(XAException.XAER_RMERR);
    }

    public int prepare(Xid xid) throws XAException
    {
        this.checkParticipantReadyIfNecessary();

        XidFactory xidFactory = this.beanFactory.getXidFactory();
        TransactionId branchXid = (TransactionId) xid;
        TransactionId globalXid = xidFactory.createGlobalTransactionId(branchXid.getGlobalTransactionId());
        TransactionRepository repository = beanFactory.getTransactionRepository();
        Transaction transaction = repository.getTransaction(globalXid);
        if (transaction == null)
        {
            throw new XAException(XAException.XAER_NOTA);
        }

        try
        {
            return transaction.participantPrepare();
        } catch (CommitRequiredException crex)
        {
            return XA_OK;
        } catch (RollbackRequiredException rrex)
        {
            throw new XAException(XAException.XAER_RMERR);
        }
    }

    public Xid[] recover(int flag) throws XAException
    {
        this.checkParticipantReadyIfNecessary();

        TransactionRepository repository = beanFactory.getTransactionRepository();
        List<Transaction> allTransactionList = repository.getActiveTransactionList();

        List<Transaction> transactions = new ArrayList<Transaction>();
        for (int i = 0; i < allTransactionList.size(); i++)
        {
            Transaction transaction = allTransactionList.get(i);
            int transactionStatus = transaction.getTransactionStatus();
            if (transactionStatus == Status.STATUS_PREPARED || transactionStatus == Status.STATUS_COMMITTING
                    || transactionStatus == Status.STATUS_ROLLING_BACK || transactionStatus == Status.STATUS_COMMITTED
                    || transactionStatus == Status.STATUS_ROLLEDBACK)
            {
                transactions.add(transaction);
            } else if (transaction.getTransactionContext().isRecoveried())
            {
                transactions.add(transaction);
            }
        }

        TransactionId[] xidArray = new TransactionId[transactions.size()];
        for (int i = 0; i < transactions.size(); i++)
        {
            Transaction transaction = transactions.get(i);
            xidArray[i] = transaction.getTransactionContext().getXid();
        }

        return xidArray;
    }

    public void rollback(Xid xid) throws XAException
    {
        this.checkParticipantReadyIfNecessary();

        XidFactory xidFactory = this.beanFactory.getXidFactory();
        TransactionId branchXid = (TransactionId) xid;
        TransactionId globalXid = xidFactory.createGlobalTransactionId(branchXid.getGlobalTransactionId());
        TransactionRepository repository = beanFactory.getTransactionRepository();
        Transaction transaction = repository.getTransaction(globalXid);
        if (transaction == null)
        {
            throw new XAException(XAException.XAER_NOTA);
        }

        try
        {
            transaction.participantRollback();
            transaction.forgetQuietly(); // forget transaction
        } catch (RollbackRequiredException rrex)
        {
            logger.error("[{}] Error occurred while rolling back remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), rrex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(rrex);
            throw xaex;
        } catch (SystemException ex)
        {
            logger.error("[{}] Error occurred while rolling back remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), ex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(ex);
            throw xaex;
        } catch (RuntimeException rrex)
        {
            logger.error("[{}] Error occurred while rolling back remote coordinator.",
                    ByteUtils.byteArrayToString(xid.getGlobalTransactionId()), rrex);
            repository.putErrorTransaction(globalXid, transaction);

            XAException xaex = new XAException(XAException.XAER_RMERR);
            xaex.initCause(rrex);
            throw xaex;
        }
    }

    public void markParticipantReady()
    {
        try
        {
            this.lock.lock();
            this.ready = true;
        } finally
        {
            this.lock.unlock();
        }
    }

    private void checkParticipantReadyIfNecessary() throws XAException
    {
        if (this.ready == false)
        {
            this.checkParticipantReady();
        }
    }

    private void checkParticipantReady() throws XAException
    {
        try
        {
            this.lock.lock();
            if (this.ready == false)
            {
                throw new XAException(XAException.XAER_RMFAIL);
            }
        } finally
        {
            this.lock.unlock();
        }
    }

    public boolean setTransactionTimeout(int seconds) throws XAException
    {
        return false;
    }

    public void setEndpoint(String identifier)
    {
        this.endpoint = identifier;
    }

    public String getIdentifier()
    {
        return this.endpoint;
    }

    public String getApplication()
    {
        return this.endpoint;
    }

    public void setBeanFactory(TransactionBeanFactory tbf)
    {
        this.beanFactory = tbf;
    }

}
